本系列文已改編成書「Arduino 自造趣:結合 JavaScript x Vue x Phaser 輕鬆打造個人遊戲機」,本書改用 Vue3 與 TypeScript 全面重構且加上更詳細的說明,
在此感謝 iT 邦幫忙、博碩文化與編輯小 p 的協助,歡迎大家前往購書,鱈魚在此感謝大家 (。・∀・)。
若想 DIY 卻不知道零件去哪裡買的讀者,可以參考此連結 。( •̀ ω •́ )✧
讓我們透過 Web Serial API 連接 COM 吧!
首先來建立頁面。
準備撰寫組件之前,先來建立一些全局共用的樣式變數與 Class。
直接將共用變數新增至 Quasar 已存在的檔案 src\styles\quasar.variables.sass 中:
$primary   : #027BE3
$secondary : #26A69A
$accent    : #9C27B0
$dark      : #1D1D1D
$positive  : #21BA45
$negative  : #C10015
$info      : #31CCEC
$warning   : #F2C037
// 整體視覺以圓角為主,建立基準值
$border-radius-s: 12px
$border-radius-m: 20px
@import '~quasar-variables-styl'
建立檔案 src\styles\global.sass
// 引入變數
@import '@/styles/quasar.variables.sass'
.c-row
  display: flex
.c-col
  display: flex
  flex-direction: column
// 圓角基準樣式
.border-radius-m
  border-radius: $border-radius-m !important
.border-radius-s
  border-radius: $border-radius-s !important
// 滾動條樣式
::-webkit-scrollbar 
  width: 3px
  height: 3px  
::-webkit-scrollbar-track
  padding: 5px
  border-radius: 7.5px
::-webkit-scrollbar-thumb 
  border-radius: 7.5px
最後在 src\main.js  引入 global.sass
import Vue from 'vue';
import App from './app.vue';
import router from './router/router';
import store from './store/store';
import './quasar';
import i18n from './i18n';
import '@/styles/global.sass';
import 'windi.css';
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  i18n,
  render: (h) => h(App),
}).$mount('#app');
由於 Web Serial API 建立成功後的 Serial Port 物件會被多個卡片共用,所以在 Vuex 中建立 core 模組,用來儲存 Port 與各類系統設定。
src\store\modules\core.store.js
/**
 * 管理 Port 物件、系統主要設定
 */
/**
 * @typedef {import('vuex').Module} Module
 */
/** @type {Module} */
const self = {
  namespaced: true,
  state: () => ({
    port: null,
  }),
  mutations: {
    setPort(state, port) {
      state.port = port;
    },
  },
  actions: {
  },
  modules: {
  },
};
export default self;
並在 Vuex 中的 modules 引入 core.store.js。
src\store\store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import core from './modules/core.store';
export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    core,
  },
});
接著建立用來選擇 Port 的設定對話框,這裡使用 Quasar Dialog 組件。
功能需求為:
src\components\dialog-system-setting.vue <script>
import { mapState } from 'vuex';
import to from 'safe-await';
export default {
  name: 'DialogSystemSetting',
  components: {},
  props: {},
  data() {
    return {};
  },
  computed: {
    ...mapState({
      port: (state) => state.core.port,
    }),
    notSupportSerialApi() {
      return !navigator?.serial;
    },
    /** 判斷 Dialog 是否可以關閉 */
    isPersistent() {
      if (this.errMsg) {
        return true;
      }
      return false;
    },
    errMsg() {
      if (this.notSupportSerialApi) {
        return '瀏覽器不支援 Web Serial API';
      }
      if (!this.port) {
        return '請選擇 COM Port';
      }
      return null;
    },
  },
  watch: {
    /** 如果 Dialog 可以關閉,則自動關閉 */
    isPersistent(val) {
      if (!val) {
        this.hide();
      }
    },
  },
  created() {},
  mounted() {},
  methods: {
    hide() {
      this.$refs.dialog.hide();
    },
    /** 開啟外部連結 */
    openRefWeb() {
      window.open('https://caniuse.com/?search=Web%20serial%20API');
    },
    /** 請求選擇 COM */
    async requestPort() {
      /** 請求連線 Port
       * https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#checking_for_available_ports
       */
      const [err, port] = await to(navigator.serial.requestPort());
      if (err) {
        // 使用者取消選擇不彈出錯誤提示
        if (`${err}`.includes('No port selected by the user')) {
          return;
        }
        /** 其餘錯誤則透過 Quasar Notify 顯示
         * 參考:https://v1.quasar.dev/quasar-plugins/notify
         */
        this.$q.notify({
          type: 'negative',
          message: `選擇 COM Port 發生錯誤 : ${err}`,
        });
        console.error(`[ requestPort ] err : `, err);
        return;
      }
      // 儲存至 Vuex
      this.$store.commit('core/setPort', port);
    },
    /** 處理 Dialog shake 事件
     * 參考 https://v1.quasar.dev/vue-components/dialog#different-modes
     */
    handleShake() {
      if (!this.errMsg) {
        return;
      }
      this.$q.notify({
        type: 'negative',
        message: this.errMsg,
      });
    },
  },
};
src\components\dialog-system-setting.vue <template lang="pug">
q-dialog(value, :persistent='isPersistent', @shake='handleShake', ref='dialog')
  // 如果瀏覽器不支援 Web Serial API,顯示此區塊
  q-card.border-radius-m(v-if='notSupportSerialApi')
    q-card-section.c-col.flex-center.p-30px
      .text-18px.text-red.mb-30px
        | 瀏覽器不支援 Web Serial API,請改用支援此 API 之瀏覽器
      q-btn(@click='openRefWeb()', color='primary') 參考資料
  // 否則顯示此區塊
  q-card.border-radius-m(v-else)
    q-card-section.min-w-450px
      q-list
        q-item-label(header)
          | 系統設定
        q-item.border-radius-m(@click='requestPort', clickable)
          q-item-section(avatar)
            q-icon(name='r_developer_board', color='grey-7')
          q-item-section
            q-item-label
              | 選擇 COM Port
            q-item-label(caption)
              | 點擊選擇指定 COM Port
          q-item-section(v-if='!port', side)
            q-icon(name='r_error', color='red')
safe-await是一種包裝await的方法,可以在不使用try catch的情況下使用await,配合Return Early Pattern,個人覺得這樣可讀性比較好 (´,,•ω•,,)
詳細探討過程與介紹可以參考作者的 Github
將 dialog-system-setting.vue 組件引入 src\app.vue 中:
src\app.vue <script>
import DialogSystemSetting from '@/components/dialog-system-setting.vue';
export default {
  name: 'App',
  components: {
    'dialog-system-setting': DialogSystemSetting,
  },
  data() {
    return {};
  },
  computed: {},
  watch: {},
  created() {},
  mounted() {},
  methods: {},
};
src\app.vue <template lang="pug">
.screen
  dialog-system-setting
成功的話,目前畫面應該會像下圖:

瀏覽器不支援 API 的話,會變這樣子。

如果沒有選擇 Port,點擊 Dialog 以外的地方會有錯誤訊息跳出。

如果點擊「選擇 COM Port」,則瀏覽器會跳出彈出視窗,選擇 Port 後會自動關閉 Dialog。

以上我們完成 Serial API 的第一步驟「選擇 Port」了!
navigator.serial.requestPort() 取得 COM 存取權限以上程式碼已同步至 GitLab,大家可以前往下載: